<?php
// $Id: invite.module,v 1.25.2.5 2009/04/19 20:51:04 sun Exp $

/**
 * @file
 * Allows your users to send and track invitations to join your site.
 */

/**
 * Session names.
 */
define('INVITE_SESSION', 'invite_code');
define('INVITE_ADMIN_SESSION', 'invite_admin_filter');

/**
 * Value for unlimited invites.
 */
define('INVITE_UNLIMITED', -1);

/**
 * Include token support.
 */
require_once drupal_get_path('module', 'invite') .'/invite_token.inc';

/**
 * Implementation of hook_help().
 */
function invite_help($path, $arg) {
  switch ($path) {
    // Display module help
    case 'admin/help#invite':
      return _invite_module_help();

    // Display introductory text on user profile pages
    case 'user/%/invites':
    case 'user/%/invites/accepted':
      $output = '<p>'. t("The invitations shown on this page have been used to join the site. Clicking on an e-mail address takes you to the user's profile page.");
      break;
    case 'user/%/invites/pending':
      $output = '<p>'. t("The invitations shown on this page haven't been accepted yet.");
      break;
    case 'user/%/invites/expired':
      $output = '<p>'. t('The invitations shown on this page have not been used to register on the site within the expiration period of @count days.', array('@count' => variable_get('invite_expiry', 30)));
      break;

    default:
      return;
  }
  $output .= ' '. t('The status <em>deleted</em> means the user account has been terminated.') .'</p>';
  if (!user_access('withdraw accepted invitations')) {
    $output .= '<p>'. t("At any time, you may withdraw either pending or expired invitations. Accepted invitations can't be withdrawn and count permanently toward your invitation allotment.") .'</p>';
  }
  return $output;
}

/**
 * Display module help.
 */
function _invite_module_help() {
  $file = drupal_get_path('module', 'invite') .'/README.txt';
  if (file_exists($file)) {
    return _filter_autop(check_plain(file_get_contents($file)));
  }
}

/**
 * Implementation of hook_theme().
 */
function invite_theme() {
  return array(
    'invite_form' => array(
      'arguments' => array('form' => NULL),
    ),
    'invite_user_overview' => array(
      'arguments' => array('items' => NULL),
      'file' => 'invite_admin.inc',
    ),
    'invite_token_help' => array(
      'arguments' => array('type' => NULL, 'prefix' => NULL, 'suffix' => NULL),
      'file' => 'invite_token.inc',
    ),
  );
}

/**
 * Implementation of hook_perm().
 */
function invite_perm() {
  return array(
    'send invitations',
    'send mass invitations',
    'track invitations',
    'withdraw accepted invitations'
  );
}

/**
 * Implements hook_init().
 */
function invite_init() {
  global $user;

  // Notify current user about newly joined invitees.
  if (!empty($user->invite_sent) && !module_invoke('throttle', 'status')) {
    invite_notify($user->uid);
  }
}

/**
 * Implementation of hook_menu().
 */
function invite_menu() {
  // Admin menu items
  $items['admin/user/invite'] = array(
    'title' => 'Invites',
    'page callback' => 'invite_admin_overview',
    'access arguments' => array('administer site configuration'),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'invite_admin.inc',
  );
  $items['admin/user/invite/list'] = array(
    'title' => 'Inviters',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items['admin/user/invite/settings'] = array(
    'title' => 'Settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('invite_settings'),
    'access arguments' => array('administer site configuration'),
    'type' => MENU_LOCAL_TASK,
    'weight' => 10,
    'file' => 'invite_admin.inc',
  );
  $items['admin/user/invite/details/%user'] = array(
    'title callback' => 'invite_admin_details_page_title',
    'title arguments' => array(4),
    'page callback' => 'invite_admin_details',
    'page arguments' => array(4),
    'access arguments' => array('administer site configuration'),
    'type' => MENU_LOCAL_TASK,
    'file' => 'invite_admin.inc',
  );

  // Frontend menu items
  $items['invite'] = array(
    'title' => 'Invite a friend',
    'title callback' => 'invite_page_title',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('invite_form', 'page', array()),
    'access arguments' => array('send invitations'),
    'type' => MENU_NORMAL_ITEM,
  );
  $items['invite/accept/%invite'] = array(
    'page callback' => 'invite_accept',
    'page arguments' => array(2),
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  $items['invite/withdraw'] = array(
    'page callback' => 'drupal_get_form',
    'page arguments' => array('invite_cancel'),
    'access arguments' => array('track invitations'),
    'type' => MENU_CALLBACK,
  );
  $items['invite/resend/%invite'] = array(
    'title' => 'Resend invitation',
    'page callback' => 'invite_resend',
    'page arguments' => array(2),
    'access arguments' => array('send invitations'),
    'type' => MENU_CALLBACK,
  );

  // User profile tabs
  $items['user/%user/invites'] = array(
    'title' => 'Invitations',
    'page callback' => 'invite_user_overview',
    'access callback' => 'invite_access_callback',
    'access arguments' => array('track invitations', 1),
    'type' => MENU_LOCAL_TASK,
    'file' => 'invite_admin.inc',
  );
  $items['user/%user/invites/accepted'] = array(
    'title' => 'Accepted',
    'page callback' => 'invite_user_overview',
    'page arguments' => array('accepted'),
    'access callback' => 'invite_access_callback',
    'access arguments' => array('track invitations', 1),
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -5,
    'file' => 'invite_admin.inc',
  );
  $items['user/%user/invites/pending'] = array(
    'title' => 'Pending',
    'page callback' => 'invite_user_overview',
    'page arguments' => array('pending'),
    'access callback' => 'invite_access_callback',
    'access arguments' => array('track invitations', 1),
    'type' => MENU_LOCAL_TASK,
    'file' => 'invite_admin.inc',
  );
  $items['user/%user/invites/expired'] = array(
    'title' => 'Expired',
    'page callback' => 'invite_user_overview',
    'page arguments' => array('expired'),
    'access callback' => 'invite_access_callback',
    'access arguments' => array('track invitations', 1),
    'type' => MENU_LOCAL_TASK,
    'weight' => 5,
    'file' => 'invite_admin.inc',
  );
  $items['user/%user/invites/new'] = array(
    'title' => 'New invitation',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('invite_form', 'page', array()),
    'access callback' => 'invite_access_callback',
    'access arguments' => array('send invitations', 1),
    'type' => MENU_LOCAL_TASK,
    'weight' => 10,
  );

  return $items;
}

/**
 * Title callback allowing for customization of the invite page title.
 *
 * @param $title
 *   The default page title, ie. non-overridden.
 */
function invite_page_title($title) {
  return variable_get('invite_page_title', $title);
}

/**
 * Title callback for the user details administration page.
 *
 * @param $account
 */
function invite_admin_details_page_title($account) {
  return t('Invitees of @name', array('@name' => $account->name));
}

/**
 * Access callback ensuring the user profile tabs are visible only to their
 * owner.
 *
 * @param $permission
 *   Required permission to view the item.
 * @param $account
 *   A user object.
 */
function invite_access_callback($permission, $account) {
  return ($account->uid == $GLOBALS['user']->uid && user_access($permission));
}

/**
 * Displays a notification message when an invited user has registered.
 *
 * @param $uid
 *   The user id to check accepted invitations for.
 */
function invite_notify($uid) {
  $result = db_query('SELECT invitee FROM {invite_notifications} WHERE uid = %d', $uid);
  while ($row = db_fetch_object($result)) {
    $account = user_load(array('uid' => $row->invitee, 'status' => 1));
    if ($account) {
      drupal_set_message(t('!user (@email) has joined @site-name!', array('!user' => theme('username', $account), '@email' => $account->mail, '@site-name' => variable_get('site_name', t('Drupal')))));
      db_query("DELETE FROM {invite_notifications} WHERE uid = %d AND invitee = %d", $uid, $row->invitee);
    }
  }
}

/**
 * Menu callback; handle incoming requests for accepting an invite.
 *
 * @param $invite
 *   A (unvalidated) invite object.
 */
function invite_accept($invite) {
  global $user;

  if (!$user->uid && invite_validate($invite)) {
    $_SESSION[INVITE_SESSION] = $invite->reg_code;
    drupal_goto('user/register');
  }

  drupal_goto();
}

/**
 * Implementation of hook_form_alter().
 */
function invite_form_alter(&$form, $form_state, $form_id) {
  switch ($form_id) {
    case 'user_admin_settings':
      // Add new registration mode.
      // We prepend the option value with a numeric value to make 3rd party
      // modules like LoginToboggan act like expected. This works because
      // checking for ('1-inviteonly' == 1) returns TRUE. To reliably determine
      // the variable value later, we need to use the strict equality operator
      // (===).
      $form['registration']['user_register']['#options']['1-inviteonly'] = t('New user registration by invitation only.');
      break;

    case 'user_register':
      // In order to prevent caching of the preset e-mail address, we have to
      // disable caching for user/register.
      $GLOBALS['conf']['cache'] = CACHE_DISABLED;

      $invite = invite_load_from_session();

      // Legacy url support (user/register/regcode).
      if (!$invite && $code = arg(2)) {
        if ($invite = invite_load($code)) {
          if (invite_validate($invite)) {
            $_SESSION[INVITE_SESSION] = $invite->reg_code;
          }
        }
      }

      if ($invite) {
        // Preset the e-mail field.
        if (isset($form['account'])) {
          $field = &$form['account'];
        }
        else {
          $field = &$form;
        }
        if (isset($field['mail'])) {
          $field['mail']['#default_value'] = $invite->email;
        }
      }
      else if (variable_get('user_register', 1) === '1-inviteonly' && !user_access('administer users')) {
        drupal_set_message(t('Sorry, new user registration by invitation only.'));
        drupal_goto();
      }
      break;

    case 'user_login_block':
      // Remove temptation for non members to try and register.
      if (variable_get('user_register', 1) === '1-inviteonly') {
        $new_items = array();
        $new_items[] = l(t('Request new password'), 'user/password', array('attributes' => array('title' => t('Request new password via e-mail.'))));
        $form['links']['#value'] = theme('item_list', $new_items);
      }
      break;
  }
}

/**
 * Load an invite record for a tracking code.
 *
 * @param $code
 *   A registration code to load the invite record for.
 * @return
 *   An invite record.
 */
function invite_load($code) {
  $result = db_query("SELECT * FROM {invite} WHERE reg_code = '%s' AND canceled = 0", $code);
  if ($invite = db_fetch_object($result)) {
    $invite->inviter = user_load(array('uid' => $invite->uid));
    $invite->data = (array)unserialize($invite->data);
  }
  return $invite;
}

/**
 * Returns an invite record from an invite code stored in the user's session.
 *
 * @return
 *   An invite record, or FALSE if there is no invite code stored in the
 *   user's session.
 */
function invite_load_from_session() {
  if (isset($_SESSION[INVITE_SESSION])) {
    return invite_load($_SESSION[INVITE_SESSION]);
  }
  return FALSE;
}

/**
 * Validates an invite record.
 *
 * @param $invite
 *   An invite record as returned by invite_load().
 * @return
 *   TRUE if the invite is valid, otherwise this function won't return.
 */
function invite_validate($invite) {
  if (!$invite || !$invite->inviter) {
    drupal_set_message(t('This invitation has been withdrawn.'));
    drupal_goto();
  }
  else if ($invite->joined != 0) {
    drupal_set_message(t('This invitation has already been used. Please login now with your username and password.'));
    drupal_goto('user');
  }
  else if ($invite->expiry < time()) {
    drupal_set_message(t('Sorry, this invitation has expired.'));
    drupal_goto();
  }
  else {
    return TRUE;
  }
}

/**
 * Implementation of hook_user().
 */
function invite_user($op, &$edit, &$account, $category = NULL) {
  switch ($op) {
    case 'insert':
      $invite = invite_load_from_session();

      if (!$invite) {
        // Try to look up an invitation in case a user has been invited to join
        // the site, but did go straight to the site and signed up without
        // using the invite link.
        $code = db_result(db_query("SELECT reg_code FROM {invite} WHERE email = '%s'", $account->mail));
        if ($code) {
          $invite = invite_load($code);
        }
      }
      if ($invite) {
        _invite_accept($invite, $account);

        // Flag the inviting user, this triggers status notifications and
        // saves us some queries otherwise.
        if ($invite->inviter->uid) {
          user_save($invite->inviter, array('invite_sent' => TRUE));
        }

        unset($_SESSION[INVITE_SESSION]);
      }
      break;

    case 'delete':
      invite_delete($account->uid);
      break;
  }
}

/**
 * Set an invitation's status to accepted.
 *
 * @param $invite
 *   An invite object.
 * @param $account
 *   The user object of the invitee.
 */
function _invite_accept($invite, $account) {
  // Update the invitation record.
  db_query("UPDATE {invite} SET email = '%s', invitee = %d, joined = %d WHERE reg_code = '%s'", $account->mail, $account->uid, time(), $invite->reg_code);
  // Delete all invites to these e-mail addresses, except this one.
  db_query("DELETE FROM {invite} WHERE (email = '%s' OR email = '%s') AND reg_code <> '%s'", $invite->email, $account->mail, $invite->reg_code);
  // Add all users who invited this particular e-mail address to the
  // notification queue.
  db_query("INSERT INTO {invite_notifications} (uid, invitee) SELECT uid, %d from {invite} WHERE (email = '%s' OR email = '%s') AND canceled = 0", $account->uid, $invite->email, $account->mail);
  // Escalate the invitee's role.
  _invite_escalate_role($account);
  // Unblock user account.
  db_query("UPDATE {users} SET status = 1 WHERE uid = %d", $account->uid);
}

/**
 * Escalates an invited user's role, based on the role(s) of the inviter.
 *
 * @param $account
 *   The user object of the invitee.
 */
function _invite_escalate_role($account) {
  // Default target role.
  $roles = array('default');

  // Add roles of inviter.
  $inviter_uid = db_result(db_query("SELECT uid FROM {invite} WHERE invitee = %d", $account->uid));
  if ($inviter_uid && $inviter = user_load(array('uid' => $inviter_uid))) {
    $roles = array_merge($roles, array_intersect($inviter->roles, user_roles(0, 'send invitations')));
  }

  // Map to configured target roles.
  $targets = array();
  foreach ($roles as $role) {
    $role_no_space = str_replace(' ', '_', $role);
    $target = variable_get('invite_target_role_'. $role_no_space, DRUPAL_AUTHENTICATED_RID);
    if ($target != DRUPAL_AUTHENTICATED_RID) {
      $targets[$target] = $target;
    }
  }

  // Notify other modules of changed user.
  $edit = array('roles' => $targets);
  user_module_invoke('update', $edit, $account);

  // Save new user role(s).
  foreach ($targets as $target) {
    db_query("DELETE FROM {users_roles} WHERE uid = %d AND rid = %d", $account->uid, $target);
    db_query("INSERT INTO {users_roles} (uid, rid) VALUES (%d, %d)", $account->uid, $target);
  }

  // Notify other modules of role escalation.
  $args = array('invitee' => $account, 'inviter' => $inviter, 'roles' => $targets);
  module_invoke_all('invite', 'escalate', $args);
}

/**
 * Physically delete all invites from and to a user.
 *
 * @param $uid
 *   The user id to delete invites for.
 */
function invite_delete($uid) {
  // Delete invite for this user if the originating user has the permission.
  $origin = db_result(db_query("SELECT uid FROM {invite} WHERE invitee = %d", $uid));
  if ($origin && $inviter = user_load(array('uid' => $origin))) {
    if (user_access('withdraw accepted invitations', $inviter)) {
      db_query("DELETE FROM {invite} WHERE invitee = %d", $uid);
    }
  }
  // Delete any invites originating from this user.
  db_query("DELETE FROM {invite} WHERE uid = %d", $uid);
  // Clean up the notification queue.
  db_query("DELETE FROM {invite_notifications} WHERE uid = %d OR invitee = %d", $uid, $uid);
}

/**
 * Implementation of hook_block().
 */
function invite_block($op = 'list', $delta = 0, $edit = array()) {
  if ($op == 'list') {
    $blocks[0] = array('info' => t('Invite a friend'), 'cache' => BLOCK_CACHE_PER_ROLE);
    return $blocks;
  }
  else if ($op == 'view') {
    $block = array();
    switch ($delta) {
      case 0:
        if (user_access('send invitations')) {
          $block = array(
            'subject' => t('Invite a friend'),
            'content' => drupal_get_form('invite_form', 'block'),
          );
        }
        break;
    }
    return $block;
  }
}

/**
 * Generate the invite forms.
 *
 * @param $form_satate
 *   A keyed array containing the current state of the form.
 * @param $op
 *   The type of form to generate, 'page' or 'block'.
 * @param $edit
 *   Previous values when resending an invite.
 * @return
 *   A form definition.
 */
function invite_form(&$form_state, $op = 'page', $edit = array()) {
  global $user;

  if (!is_array($edit)) {
    $edit = (array)$edit;
  }

  $remaining_invites = invite_get_remaining_invites($user);

  if ($remaining_invites == 0) {
    if ($op == 'block') {
      // Hide block.
      $form['#access'] = FALSE;
      return $form;
    }
    else if (!$edit) {
      // Deny access when NOT resending an invite.
      drupal_set_message(t("Sorry, you've reached the maximum number of invitations."), 'error');
      drupal_goto(referer_uri());
    }
  }

  $form['resent'] = array(
    '#type' => 'value',
    '#value' => $edit ? $edit['resent'] + 1 : 0,
  );
  $form['reg_code'] = array(
    '#type' => 'value',
    '#value' => $edit ? $edit['reg_code'] : NULL,
  );
  if ($remaining_invites != INVITE_UNLIMITED) {
    $form['remaining_invites'] = array(
      '#type' => 'value',
      '#value' => $remaining_invites,
    );
  }
  switch ($op) {
    case 'page':
    default:
      $form += invite_page_form($remaining_invites, $edit);
      break;
    case 'block':
      $form += invite_block_form($remaining_invites);
      break;
  }

  return $form;
}

/**
 * Calculate the remaining invites of a user.
 *
 * @param $account
 *   A user object.
 * @return
 *   The number of remaining invites.
 */
function invite_get_remaining_invites($account) {
  if ($account->uid == 1) {
    return INVITE_UNLIMITED;
  }

  // Check user property for remaining invites.
  $data = unserialize($account->data);
  if (isset($data['invites'])) {
    $remaining = $data['invites'];
  }
  else {
    $remaining = invite_get_role_limit($account);

    if ($remaining > 0) {
      // Legacy support.
      $sent = db_result(db_query("SELECT COUNT(*) FROM {invite} WHERE uid = %d", $account->uid));
      $remaining = max($remaining - $sent, 0);
      if ($sent > 0) {
        // Update user property for faster lookup next time.
        user_save($account, array('invites' => $remaining));
      }
    }
  }

  return $remaining;
}

/**
 * Calculate the max. number of invites based on a user's role.
 *
 * @param $account
 *   A user object.
 * @return
 *   The configured maximum of invites.
 */
function invite_get_role_limit($account) {
  if (!isset($account->roles)) {
    $account = user_load(array('uid' => $account->uid));
  }

  $role_limit = 0;
  foreach (user_roles(0, 'send invitations') as $role) {
    $role_no_space = str_replace(' ', '_', $role);
    if (in_array($role, $account->roles)) {
      $role_max = variable_get('invite_maxnum_'. $role_no_space, INVITE_UNLIMITED);
      if ($role_max == INVITE_UNLIMITED) {
        return INVITE_UNLIMITED;
      }
      $role_limit = max($role_max, $role_limit);
    }
  }
  return $role_limit;
}

/**
 * Generate the invite page form.
 *
 * @param $remaining_invite
 *   Number of remaining invites.
 * @param $edit
 *   Previous values when resending an invite.
 * @return
 *   A form definition.
 */
function invite_page_form($remaining_invites, $edit = array()) {
  global $user;

  // Remaining invites.
  if ($remaining_invites != INVITE_UNLIMITED) {
    $form['remaining_invites_markup']['#value'] = format_plural($remaining_invites, 'You have 1 invite remaining.', 'You have @count invites remaining.');
  }

  // Sender e-mail address.
  if ($user->uid && variable_get('invite_use_users_email', 0)) {
    $from = $user->mail;
  }
  else {
    $from = variable_get('site_mail', ini_get('sendmail_from'));
  }
  // Personalize displayed e-mail address.
  // @see http://drupal.org/project/pmail
  if (module_exists('pmail')) {
    $from = personalize_email($from);
  }
  $form['from'] = array(
    '#type' => 'item',
    '#title' => t('From'),
    '#value' => check_plain($from),
  );

  // Recipient email address.
  if (!$edit) {
    $failed_emails = '';
    $allow_multiple = user_access('send mass invitations');
    if (isset($_SESSION['invite_failed_emails'])) {
      $failed_emails = implode("\n", (array)unserialize($_SESSION['invite_failed_emails']));
      unset($_SESSION['invite_failed_emails']);
    }
    $form['email'] = array(
      '#title' => t('To'),
      '#default_value' => $failed_emails,
      '#description' => format_plural($allow_multiple ? 99 : 1, 'Type the e-mail address of the person you would like to invite.', 'Type the e-mail addresses of the persons you would like to invite. Addresses should be separated by newlines or commas.'),
      '#required' => TRUE,
    );
    if ($allow_multiple) {
      $form['email']['#type'] = 'textarea';
      $form['email']['#rows'] = 3;
    }
    else {
      $form['email']['#type'] = 'textfield';
      $form['email']['#maxlength'] = 64;
    }
    if ($failed_emails) {
      $form['email']['#attributes']['class'] = 'error';
    }
  }
  else {
    // The email is not editable when resending an invite.
    $allow_multiple = FALSE;
    $form['email_markup'] = array(
      '#type' => 'item',
      '#title' => t('To'),
      '#value' => check_plain($edit['email']),
    );
    $form['email'] = array(
      '#type' => 'value',
      '#value' => $edit['email'],
    );
  }

  // Message subject.
  if ($edit && !empty($edit['data']['subject'])) {
    $subject = $edit['data']['subject'];
  }
  else {
    $subject = invite_get_subject();
  }
  // Add prefix.
  $prefix = t('Re:');
  if ($edit && drupal_substr($subject, 0, strlen($prefix)) != $prefix) {
    $subject = $prefix .' '. $subject;
  }
  if (variable_get('invite_subject_editable', FALSE)) {
    $form['subject'] = array(
      '#type' => 'textfield',
      '#title' => t('Subject'),
      '#default_value' => $subject,
      '#maxlength' => 64,
      '#description' => t('Type the subject of the invitation e-mail.'),
      '#required' => TRUE,
    );
  }
  else {
    $form['subject'] = array(
      '#type' => 'item',
      '#title' => t('Subject'),
      '#value' => check_plain($subject),
    );
  }

  // Message body.
  $form['body'] = array(
    '#type' => 'item',
    '#title' => t('Message'),
  );
  $form['message'] = array(
    '#type' => 'textarea',
    '#default_value' => ($edit && !empty($edit['data']['message'])) ? $edit['data']['message'] : '',
    '#description' => format_plural($allow_multiple ? 1 : 99, 'This message will be added to the mail sent to the person you are inviting.', 'This message will be added to the mail sent to the persons you are inviting.'),
  );

  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Send invite'),
  );

  return $form;
}

/**
 * Generate the invite block form.
 *
 * @param $remaining_invite
 *   Number of remaining invites.
 * @return
 *   A form definition.
 */
function invite_block_form($remaining_invites) {
  global $user;

  $form['#action'] = url('invite');

  $form['invite'] = array(
    '#value' => t('Recommend @site-name to:', array('@site-name' => variable_get('site_name', t('Drupal')))),
  );
  $description = '';
  if ($remaining_invites != INVITE_UNLIMITED) {
    $description = format_plural($remaining_invites, '1 invite remaining', '@count invites remaining');
  }
  $form['email'] = array(
    '#type' => 'textfield',
    '#size' => 20,
    '#maxlength' => 64,
    '#description' => $description,
    '#required' => TRUE,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Send invite'),
  );
  $form['link'] = array(
    '#prefix' => '<div><small>',
    '#value' => l(t('View your invites'), "user/$user->uid/invites"),
    '#suffix' => '</small></div>',
    '#access' => user_access('track invitations') && $user->uid,
  );

  return $form;
}

/**
 * Theme function for the invite form.
 *
 * @ingroup themeable
 */
function theme_invite_form($form) {
  $output = '';
  $op = $form['#parameters'][2];

  if ($op == 'page') {
    // Show form elements.
    $output .= drupal_render($form['remaining_invites_markup']);
    $output .= drupal_render($form['remaining_invites']);
    $output .= drupal_render($form['from']);
    if (isset($form['email_markup'])) {
      $output .= drupal_render($form['email_markup']);
    }
    $output .= drupal_render($form['email']);
    $output .= drupal_render($form['subject']);

    // Show complete invitation message.
    $output .= drupal_render($form['body']);
    $output .= '<div class="invite-message"><div class="opening">';

    // Prepare invitation message.
    $message_form = "</p></div>\n". drupal_render($form['message']) ."\n".'<div class="closing"><p>';
    $body = _filter_autop(t(_invite_get_mail_template()));

    // Perform token replacement on message body.
    $types = _invite_token_types(array('data' => array('message' => $message_form)));
    $output .= token_replace_multiple($body, $types);

    $output .= "</div></div>\n";
  }

  // Render all missing form elements.
  $output .= drupal_render($form);

  return $output;
}

/**
 * Forms API callback; validate submitted form data.
 *
 * Filters out e-mails that are already registered or have been invited before.
 * Checks the invite limit of the user and the max. number of invites per turn.
 */
function invite_form_validate($form, &$form_state) {
  global $user;

  $emails = _invite_get_emails($form_state['values']['email']);

  if (!$form_state['values']['resent']) {
    if (count($emails) > 0) {
      // Filter out already registered users, but pass validation.
      $failed_emails = _invite_validate_emails("SELECT mail AS email FROM {users} WHERE mail IN (". db_placeholders($emails, 'varchar') .")", $emails);
      if (count($failed_emails)) {
        $error = format_plural(count($failed_emails), 'The following recipient is already a member:', 'The following recipients are already members:') .'<br />';
        foreach ($failed_emails as $key => $email) {
          $account = user_load(array('mail' => $email));
          $failed_emails[$key] = theme('username', $account) .' ('. check_plain($email) .')';
        }
        $error .= implode(', ', $failed_emails);
        drupal_set_message($error, 'error');
      }
    }

    if (!empty($emails)) {
      // Filter out already invited users, but pass validation.
      $failed_emails = _invite_validate_emails("SELECT email FROM {invite} WHERE email IN (". db_placeholders($emails, 'varchar') .") AND uid = %d AND canceled = 0", $emails, $user->uid);
      if (count($failed_emails)) {
        $error = format_plural(count($failed_emails), 'You did already invite the following recipient:', 'You did already invite the following recipients:') .'<br />';
        $error .= implode(', ', array_map('check_plain', $failed_emails));
        drupal_set_message($error, 'error');
      }
    }

    // Check that there is at least one valid e-mail remaining after filtering
    // out dupes.
    if (count($emails) == 0) {
      form_set_error('email');
      return;
    }

    // Check invite limit, fail to let the user choose which ones to send.
    if (isset($form_state['values']['remaining_invites']) && count($emails) > $form_state['values']['remaining_invites']) {
      form_set_error('email', format_plural($form_state['values']['remaining_invites'], 'You have only 1 invite left.', 'You have only @count invites left.'));
      return;
    }

    // Check number of e-mails.
    if (!user_access('send mass invitations') && count($emails) > 1) {
      form_set_error('email', t('You cannot send more than one invitation.'));
      return;
    }
  }

  // Save valid emails.
  $form_state['values']['valid_emails'] = $emails;
}

/**
 * Extract valid e-mail addresses from a string.
 *
 * E-mails must be separated by newlines or commas. E-mails are allowed to
 * include a display name (eg. Some Name <foo@example.com>). Invalid addresses
 * are filtered out and stored in a session variable for re-display.
 *
 * @param $string
 *   The string to process. Recognized delimiters are comma, NL and CR.
 * @return
 *   Array of valid e-mail addresses.
 */
function _invite_get_emails($string) {
  $valid_emails = $failed_emails = array();
  $user = '[a-zA-Z0-9_\-\.\+\^!#\$%&*+\/\=\?\`\|\{\}~\']+';
  $domain = '(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.?)+';
  $ipv4 = '[0-9]{1,3}(\.[0-9]{1,3}){3}';
  $ipv6 = '[0-9a-fA-F]{1,4}(\:[0-9a-fA-F]{1,4}){7}';
  $rx = "/($user@($domain|(\[($ipv4|$ipv6)\])))>?$/";

  $emails = array_unique(split("[,\n\r]", $string));
  foreach ($emails as $email) {
    $email = trim($email);
    if ($email) {
      if (preg_match($rx, $email, $match)) {
        $valid_emails[] = $match[1];
      }
      else {
        $failed_emails[] = $email;
      }
    }
  }

  if (count($failed_emails)) {
    $_SESSION['invite_failed_emails'] = serialize($failed_emails);
  }

  return $valid_emails;
}

/**
 * Filter out e-mails based on a database query.
 *
 * @param $sql
 *   The query to execute.
 * @param &$emails
 *   The list of e-mail addresses to validate. When this function returns, all
 *   invalid e-mails have already been removed.
 * @param ...
 *   More query arguments.
 * @return
 *   An array of invalid e-mail addresses.
 */
function _invite_validate_emails($sql, &$emails) {
  $failed_emails = array();
  // Build query arguments.
  $args = func_get_args();
  $args = array_merge($emails, array_slice($args, 2));
  $result = db_query($sql, $args);
  while ($row = db_fetch_object($result)) {
    $failed_emails[] = $row->email;
  }
  // Keep only valid e-mails.
  $emails = array_diff($emails, $failed_emails);
  return $failed_emails;
}

/**
 * Forms API callback; process submitted form data.
 */
function invite_form_submit($form, &$form_state) {
  global $user, $language;

  // Set this now, so other modules can change it later.
  $form_state['redirect'] = 'invite';

  $failed_emails = array();
  $num_failed = $num_succeeded = 0;

  // Get e-mails that failed validation.
  if (isset($_SESSION['invite_failed_emails'])) {
    $failed_emails = (array)unserialize($_SESSION['invite_failed_emails']);
    $num_failed = count($failed_emails);
  }

  $subject = isset($form_state['values']['subject']) ? trim($form_state['values']['subject']) : invite_get_subject();
  $message = isset($form_state['values']['message']) ? trim($form_state['values']['message']) : NULL;

  if (!variable_get('invite_use_users_email', 0)) {
    $from = variable_get('invite_manual_from', '');
  }
  else if ($user->uid) {
    $from = $user->mail;
  }
  if (!$from) {
    // Never pass an empty string to drupal_mail()
    $from = NULL;
  }

  foreach ($form_state['values']['valid_emails'] as $email) {
    // Create the invite object.
    $code = $form_state['values']['reg_code'] ? $form_state['values']['reg_code'] : invite_generate_code();
    $invite = _invite_substitutions(array(
      'email' => $email,
      'code'  => $code,
      'resent'  => $form_state['values']['resent'],
      'data'  => array('subject' => $subject, 'message' => $message),
    ));

    // Send e-mail.
    $params = array('invite' => $invite);
    $message = drupal_mail('invite', 'invite', $email, $language, $params, $from, TRUE);
    if (1 || $message['result']) {
      // Save invite.
      invite_save($invite);

      // Notify other modules.
      if (!$form_state['values']['resent']) {
        $args = array('inviter' => $invite->inviter, 'email' => $invite->email, 'code' => $invite->code);
        module_invoke_all('invite', 'invite', $args);
      }

      $num_succeeded++;
    }
    else {
      $failed_emails[] = $email;
    }
  }

  // Store failed e-mails for re-display.
  if ($failed_emails) {
    $_SESSION['invite_failed_emails'] = serialize($failed_emails);
  }

  if ($num_succeeded) {
    if (isset($form_state['values']['remaining_invites'])) {
      // Update user property if user is limited.
      user_save($user, array('invites' => $form_state['values']['remaining_invites'] - $num_succeeded));
    }
    $message = format_plural($num_succeeded, 'Your invitation has been successfully sent. You will be notified when the invitee joins the site.', '@count invitations have been successfully sent. You will be notified when any invitee joins the site.');
    drupal_set_message($message);
  }
  if ($num_failed) {
    $message = format_plural($num_failed, 'The entered e-mail address is invalid. Please correct it.', '@count entered e-mail addresses are invalid. Please correct them.');
    drupal_set_message($message, 'error');
  }
  else if (user_access('track invitations') && $user->uid) {
    // Everything went well: redirect to pending invites page.
    $form_state['redirect'] = "user/$user->uid/invites/pending";
  }
}

/**
 * Return the invite e-mail subject.
 *
 * @param $substitutions
 *   Associative array of substitutions for token replacement.
 * @return
 *   The e-mail subject.
 */
function invite_get_subject($substitutions = array()) {
  $subject = t(variable_get('invite_subject', t('[inviter-raw] has sent you an invite!')));
  return token_replace_multiple($subject, _invite_token_types($substitutions));
}

/**
 * Generates a unique tracking code.
 *
 * @return
 *   An 8-digit unique tracking code.
 */
function invite_generate_code() {
  do {
    $reg_code = user_password(8);
    $result = db_query("SELECT COUNT(*) FROM {invite} WHERE reg_code = '%s'", $reg_code);
  } while (db_result($result));

  return $reg_code;
}

/**
 * Implementation of hook_mail().
 */
function invite_mail($key, &$message, $params) {
  global $user;

  $invite = $params['invite'];

  // Override Reply-To address.
  if (!variable_get('invite_use_users_email_replyto', 0)) {
    $reply_to = variable_get('invite_manual_reply_to', '');
  }
  else if ($user->uid) {
    $reply_to = $user->mail;
  }
  if ($reply_to) {
    $message['headers']['Reply-To'] = $reply_to;
  }

  $message['subject'] = $invite->data['subject'];

  $template = t(_invite_get_mail_template());
  $tokens = _invite_token_types($invite);
  $message['body'][] = token_replace_multiple($template, $tokens);
}

/**
 * Save an invite to the database.
 *
 * @param $edit
 *   Associative array of data to store.
 * @return
 *   The result of the database operation.
 */
function invite_save($edit) {
  $edit = (array)$edit;
  $data = serialize($edit['data']);
  $now = time();
  $expiry = $now + (variable_get('invite_expiry', 30) * 60 * 60 * 24);
  if ($edit['resent']) {
    $result = db_query("UPDATE {invite} SET expiry = %d, resent = %d, data = '%s' WHERE reg_code = '%s' AND uid = %d", $expiry, $edit['resent'], $data, $edit['code'], $edit['inviter']->uid);
  }
  else {
    $result = db_query("INSERT INTO {invite} (reg_code, email, uid, created, expiry, data) VALUES ('%s', '%s', %d, %d, %d, '%s')", $edit['code'], $edit['email'], $edit['inviter']->uid, $now, $expiry, $data);
  }
  return $result;
}

/**
 * Menu callback; display confirm form to delete an invitation.
 *
 * @param $form_satate
 *   A keyed array containing the current state of the form.
 * @param $origin
 *   A string denoting the orginating status page to return the user to
 *   afterwards.
 * @param $code
 *   A registration code to remove the invitation for.
 */
function invite_cancel(&$form_state, $origin, $code) {
  global $user;

  $invite = invite_load($code);

  // Inviter must be the current user.
  if ($invite->inviter->uid == $user->uid) {
    // Verify the invitation may be deleted.
    if (!$invite->joined || user_access('withdraw accepted invitations')) {
      $form['#redirect'] = "user/$user->uid/invites/$origin";
      $form['invite'] = array(
        '#type' => 'value',
        '#value' => $invite,
      );
      $description = (!$invite->joined && $invite->expiry > time()) ? t("The invitee won't be able to register any more using this invitation.") : '';

      return confirm_form(
        $form,
        t('Are you sure you want to withdraw the invitation to %email?', array('%email' => $invite->email)),
        "user/$user->uid/invites/{$origin}",
        $description .' '. t('This action cannot be undone.'),
        t('Withdraw'),
        t('Cancel')
      );
    }
    else {
      drupal_set_message(t('Invitations to registered users cannot be withdrawn.'), 'error');
    }
  }
  else {
    watchdog('invite', 'Detected malicious attempt to delete an invitation.', array(), WATCHDOG_WARNING, l(t('view'), 'user/'. $user->uid));
    drupal_access_denied();
  }

  drupal_goto("user/$user->uid/invites/$origin");
}

/**
 * Submit handler to delete an invitation.
 */
function invite_cancel_submit($form, &$form_state) {
  $invite = $form_state['values']['invite'];

  db_query("UPDATE {invite} SET canceled = 1 WHERE reg_code = '%s'", $invite->reg_code);
  drupal_set_message(t('Invitation to %email has been withdrawn.', array('%email' => $invite->email)));

  // Notify other modules.
  $args = array('inviter' => $invite->inviter, 'email' => $invite->email, 'code' => $invite->reg_code);
  module_invoke_all('invite', 'cancel', $args);
}

/**
 * Menu callback; resend an expired invite.
 *
 * @param $invite
 *   An invitate object.
 */
function invite_resend($invite) {
  global $user;

  // Inviter must match current user and invitation must have expired.
  if ($invite->uid == $user->uid && $invite->expiry < time()) {
    return drupal_get_form('invite_form', 'page', $invite);
  }

  return drupal_access_denied();
}

/**
 * Return count of successful, pending, or unsuccessful invitations.
 *
 * @param $uid
 *   The user id to calculate count for.
 * @param $op
 *   The type of count to calculate: accepted, pending or expired.
 * @return
 *   A count.
 */
function invite_count($uid, $op) {
  switch ($op) {
    case 'accepted':
      return db_result(db_query("SELECT COUNT(*) FROM {invite} WHERE uid = %d AND joined <> 0", $uid));
    case 'pending':
      return db_result(db_query("SELECT COUNT(*) FROM {invite} WHERE uid = %d AND joined = 0 AND expiry >= %d", $uid, time()));
    case 'expired':
      return db_result(db_query("SELECT COUNT(*) FROM {invite} WHERE uid = %d AND joined = 0 AND expiry < %d", $uid, time()));
  }
}

/**
 * Implementation of hook_disable().
 */
function invite_disable() {
  if (variable_get('user_register', 1) === '1-inviteonly') {
    variable_set('user_register', 1);
    drupal_set_message(t('User registration option reset to %no_approval.', array('%no_approval' => t('Visitors can create accounts and no administrator approval is required.'))));
  }
}

/**
 * Returns the configured or default e-mail template.
 *
 * @return
 *   The localized e-mail body.
 */
function _invite_get_mail_template() {
  $template = t("Your friend, [inviter-raw], has invited you to join [site-name] at [site-url].

To become a member of [site-name], click the link below or paste it into the address bar of your browser.

[join-link]

----------
[invite-message-raw]");

  return variable_get('invite_default_mail_template', $template);
}

/**
 * Provide token types for use in invite message replacements.
 *
 * @param $args
 *   Associative array of additional arguments to merge in the invite object.
 * @return
 *   Array of token types suitable as input for token_replace().
 */
function _invite_token_types($args = array()) {
  global $user;

  if (!is_array($args)) {
    $args = (array)$args;
  }
  $invite = _invite_substitutions($args);
  return array('user' => $user, 'profile' => $user, 'invite' => $invite);
}

/**
 * Create an invite object with reasonable default values for use in
 * token replacements.
 *
 * @param $args
 *   Associative array of additional arguments to merge into the invite object.
 * @return
 *   The invite object.
 */
function _invite_substitutions($args = array()) {
  global $user;

  $defaults = array(
    'inviter' => $user,
    'email'   => '--recipient-email--',
    'code'    => '--invite-code--',
    'resent'  => 0,
    'data'    => array('subject' => NULL, 'message' => NULL),
  );
  return (object)array_merge($defaults, $args);
}

